<?php

require_once("hc_core_ClassLoader.php");

hc_core_ClassLoader::load('substrate_ContextObjectReference');
hc_core_ClassLoader::load('substrate_IContext');
hc_core_ClassLoader::load('substrate_IContextAware');
hc_core_ClassLoader::load('substrate_IContextStartupAware');
hc_core_ClassLoader::load('substrate_ContextUtil');
hc_core_ClassLoader::load('substrate_objects_IFactoryObject');
hc_core_ClassLoader::load('substrate_IPlaceholderConfigurer');

hc_core_ClassLoader::load('hc_core_FileLocator');

hc_core_ClassLoader::load('hc_logging_LogManager');
hc_core_ClassLoader::load('hc_logging_LoggerUtil');

class substrate_Context implements substrate_IContext {

    private static $CONTEXT_COUNTER = 0;

    private $contextCounter = null;

    protected $fileLocator = null;

    private $objectDefinitions = array();
    private $objectInstances = array();
    private $loadedClasses = array();
    private $recentlyDefinedObjects = array();
    private $initializedObjects = array();

    private $mapByInterface = array();

    public function __construct() {

        $this->_bmStart = microtime(true);

        $this->fileLocator = hc_core_FileLocator::CLASSPATH();

        $args = func_get_args();

        $this->contextCounter = self::$CONTEXT_COUNTER++;

        $this->import($args);

    }

    public function __destruct() {
        $this->logDebug('Context lived for ' . ( microtime(true) - $this->_bmStart ) . ' seconds');
    }

    protected $importing = 0;
    public function import($contextConfigLocations, $autoExecute = false) {

        if ( $this->importing == 0 ) {
            $originalObjectNames = $this->getRegisteredObjectNames();
        }

        if ( ! is_array($contextConfigLocations) ) {
            $contextConfigLocations = array($contextConfigLocations);
        }

        $context = $this;

        $this->importing++;
        foreach ( $contextConfigLocations as $contextConfigLocation ) {

            $contextFilePath = $this->fileLocator->find($contextConfigLocation);
            if ( file_exists($contextFilePath) ) {
                $this->logInfo('Importing object definitions for "' . $contextConfigLocation . '" from "' . $contextFilePath . '"');
                include($contextFilePath);
            } else {
                $this->logError('Could not find context configuration for "' . $contextConfigLocation . '"');
            }

        }
        $this->importing--;

        if ( $this->importing == 0 ) {
            $currentObjectNames = $this->getRegisteredObjectNames();
            $this->recentlyDefinedObjects = array_diff($currentObjectNames, $originalObjectNames);
            $this->logInfo('Added object definitions for: ' . implode(', ', $this->recentlyDefinedObjects));
        }

        if ( $autoExecute ) {
            $this->execute();
        }


    }

    public function ref($name = null) {
        if ( $name === null ) throw new Exception('Object name must be specified.');
        return new substrate_ContextObjectReference($name);
    }

    private function prepareObject($name) {
        $originalSetup = $this->objectDefinitions[$name];
        if ( $originalSetup['parent'] ) {
            $setup = $this->prepareObject($originalSetup['parent']);
        } else {
            $setup = $originalSetup;
        }
        foreach ( $originalSetup as $param => $value ) {
            if ( $param == 'properties' ) {
                $setup[$param] = array_merge($setup[$param], $value);
            } elseif ( $param == 'dependencies' ) {
                $setup[$param] = array_merge($setup[$param], $value);
            } elseif ( $param == 'constructorArgs' ) {
                if ( $setup['inheritConstructorArgs'] and $originalSetup['parent']) {
                    foreach ( $originalSetup['constructorArgs'] as $constructorArg => $constructorValue ) {
                        $setup[$param][$constructorArg] = $constructorValue;
                    }
                } else {
                    $setup[$param] = $value;
                }
            } elseif ( $param == 'className' and $value === null) {
                $setup[$param] = $setup[$param];
            } else {
                $setup[$param] = $value;
            }
        }
        foreach ( array('constructorArgs', 'properties', 'dependencies') as $key ) {
            foreach ( $setup[$key] as $i => $value ) {
                $setup[$key][$i] = $this->replacePlaceholder($value);
            }
        }
        return $setup;
    }

    public function execute() {
        $this->initializeLogging();
        foreach ( $this->objectDefinitions as $name => $setup ) {
            $objectDefinition = $this->prepareObject($name);
            $this->objectDefinitions[$name] = $objectDefinition;
            if ( ! $objectDefinition['lazyLoad'] ) {
                $this->instantiate($name);
            }

        }
        foreach ( $this->getRegisteredObjectNames() as $name ) {

            if ( $this->instantiated($name) ) {

                $this->instantiate($name);

            }

        }
    }

    protected final function generateAnonymousObjectName() {
        if ( ! isset($this->anonymousObjectNameCounter) ) {
            $this->anonymousObjectNameCounter = 0;
        }
        return '___anonymouseObject_context' . $this->contextCounter . '_object' . $this->anonymousObjectNameCounter++;
    }

    public function set() {
        $name = null;
        $setup = null;
        $args = func_get_args();
        if ( count($args) == 2 ) {
            list($name, $setup) = $args;
        } else {
            $name = $this->generateAnonymousObjectName();
            list($setup) = $args;
        }
        if ( $setup === null ) throw new Exception('$etup cannot be null.');
        if ( ! is_array($setup) ) {
            $setup = array ('className' => $setup);
        }
        $setup['name'] = $name;
        if ( ! isset($setup['includeFilename']) ) $setup['includeFilename'] = null;
        if ( ! isset($setup['abstract']) ) $setup['abstract'] = false;
        if ( ! isset($setup['parent']) ) $setup['parent'] = null;
        if ( ! isset($setup['properties']) ) $setup['properties'] = array();
        if ( ! isset($setup['constructorArgs']) ) $setup['constructorArgs'] = array();
        if ( ! isset($setup['dependencies']) ) $setup['dependencies'] = array();
        if ( ! isset($setup['inheritConstructorArgs']) ) $setup['inheritConstructorArgs'] = true;
        if ( ! isset($setup['lazyLoad']) ) $setup['lazyLoad'] = true;
        if ( ! isset($setup['className']) ) $setup['className'] = null;
        $this->objectDefinitions[$name] = $setup;
        return new substrate_ContextObjectReference($name);
    }

    public function exists($name = null) {
        return array_key_exists($name, $this->objectDefinitions);
    }

    public function instantiated($name = null) {
        return array_key_exists($name, $this->objectInstances);
    }

    public function initialized($name = null) {
        return array_key_exists($name, $this->initializedObjects);
    }

    public function get($name) {

        if ( $name === null ) throw new Exception('Object name must be specified.');

        if ( ! $this->initialized($name) ) {

            $this->initializedObjects[$name] = true;

            $object = $this->instantiate($name);

            if ( $object instanceof substrate_IContextAware ) {
                $object->informAboutContext($this);
            }

            if ( $object instanceof substrate_IContextStartupAware ) {
                $object->informAboutContextStartup($this);
            }

        }

        return $this->objectInstances[$name];

    }

    protected function instantiate($name = null) {
        if ( $name === null ) throw new Exception('Object name must be specified.');
        if ( ! isset($this->objectInstances[$name]) ) {
            $setup = $this->getObjectDefinition($name);
            $className = $setup['className'];
            $this->logDebug('Initializing object named "' . $name . '" (' . $className . ')');
            $this->loadDependantClasses($setup);

            $reflectionClass = new ReflectionClass($className);

            $constructor = $reflectionClass->getConstructor();
            $constructorArgs = array();

            $originalConstructorArgs = isset($setup['constructorArgs']) ?
                $setup['constructorArgs'] : null;

            if ( $constructor ) {

                foreach ( $constructor->getParameters() as $reflectionParamter ) {

                    $constructorArgumentName = $reflectionParamter->getName();
                    if ( isset($setup['constructorArgs'][$constructorArgumentName]) ) {
                        $constructorArgs[] = $setup['constructorArgs'][$constructorArgumentName];
                        // We no longer want to remember this constructor argument.
                        unset($originalConstructorArgs[$constructorArgumentName]);
                    } else {
                        $throwException = true;
                        $foundArgument = false;
                        $paramClass = $reflectionParamter->getClass();
                        if ( $paramClass ) {
                            $paramClassName = $paramClass->getName();
                            foreach ( $this->objectInstances as $testObject ) {
                                if ( $testObject instanceof $paramClassName ) {
                                    $throwException = false;
                                    $foundArgument = true;
                                    $constructorArgs[] = $testObject;
                                    break;
                                }
                            }
                        }
                        if ( ! $foundArgument and $reflectionParamter->allowsNull() ) {
                            $throwException = false;
                        }
                        if ( $throwException ) {
                            throw new Exception('Could not find constructor argument named "' . $constructorArgumentName . '" for object named "' . $name . '"');
                        }
                    }
                }

                if ( count($originalConstructorArgs) ) {
                    $constructorArgs = array_merge($constructorArgs, array_values($originalConstructorArgs));
                }

            }

            if ( sizeof($constructorArgs) ) {
                for ( $i = 0; $i < count($constructorArgs); $i++ ) {
                    $constructorArgs[$i] =
                        $this->getResolvedConstructorArg($constructorArgs[$i]);
                }
                $newInstance = $reflectionClass->newInstanceArgs($constructorArgs);
            } else {
                $newInstance = $reflectionClass->newInstance();
            }

            $references = array();
            foreach ( $setup['properties'] as $key => $value ) {
                $methodName = 'set' . ucfirst($key);
                if ( method_exists($newInstance, $methodName) ) {
                    if ( $value instanceof substrate_ContextObjectReference ) {
                        $references[] = array(
                            'methodName' => $methodName,
                            'contextObjectReference' => $value,
                        );
                    } else {
                        $newInstance->$methodName($value);
                    }
                }
            }

            foreach ( $setup['dependencies'] as $value ) {
                $references[] = array(
                    'methodName' => null,
                    'contextObjectReference' => $value,
                );
            }

            if ( $newInstance instanceof substrate_objects_IFactoryObject ) {
                $newInstance = $newInstance->getObject();
            }

            $this->objectInstances[$name] = $newInstance;

            if ( count($references) ) {
                // If there are references, we can try to load them now.
                // We do this AFTER we have stored our object reference
                // so that we can avoid infinite loops for objects that
                // have a dependency on each other.
                $this->loadReferences($name, $references);
            }

            $this->addInterfacetoMap(get_class($newInstance), $name);

            foreach ( class_implements($newInstance) as $implementedInterface ) {
                $this->addInterfaceToMap($implementedInterface, $name);
            }

            foreach ( class_parents($newInstance) as $parentClass ) {
                $this->addInterfaceToMap($parentClass, $name);
            }

        }

        return $this->objectInstances[$name];

    }

    protected function getResolvedConstructorArg($value = null) {

        if ( is_object($value) and $value instanceof substrate_ContextObjectReference  ) {
            return $this->get($value->getName());
        } elseif ( is_array($value) ) {
            $newArray = array();
            foreach ( $value as $i => $v ) {
                $newArray[$i] = $this->getResolvedConstructorArg($v);
            }
            return $newArray;
        }
        return $value;
    }

    protected function addInterfaceToMap($interfaceOrClass, $name) {
        if ( ! isset($this->mapByInterface[$interfaceOrClass]) ) {
            $this->mapByInterface[$interfaceOrClass] = array();
        }
        $this->mapByInterface[$interfaceOrClass][] = $name;
    }

    private function loadReferences($name, $references) {
        $object = $this->objectInstances[$name];
        foreach ( $references as $reference ) {
            $methodName = $reference['methodName'];
            $contextObjectReference = $reference['contextObjectReference'];
            $referencedObject = $this->get($contextObjectReference->getName());
            if ( $methodName !== null ) {
                $object->$methodName($referencedObject);
            }
        }
    }

    protected function loadDependantClasses($setup) {
        if ( $setup['parent'] ) {
            $this->loadDependantClasses($this->objectDefinitions[$setup['parent']]);
        }
        if ( array_key_exists('className', $setup) ) {
            $this->loadClass($setup['className'], $setup['includeFilename']);
        }
    }

    protected function loadClass($className, $includeFilename = null) {
        if ( $className === null ) throw new Exception('Class name must be specified.');
        if ( $includeFilename === null ) {
            hc_core_ClassLoader::load($className);
        } else {
            hc_core_ClassLoader::load($className, array('includeFilename' => $includeFilename));
        }
    }

    protected $logMessages = array();
    protected $logger = null;
    protected $loggingInitialized = false;
    protected function initializeLogging() {
        if ( ! $this->loggingInitialized ) {
            $this->loggingInitialized = true;
            if ( $this->exists('logManager') ) {
                $this->get('logManager');
            }
            if ( $this->exists('logger') ) {
                // If we have a logger object defined, we can
                // use that.
                $this->logger = $this->get('logger');
            } else {
                // Ask the log manager for for the default logger.
                $this->logger = hc_logging_LogManager::get();
            }

            if ( $this->logger !== null ) {
                hc_logging_LoggerUtil::SET_ERROR_HANDLER($this->logger);
                foreach ( $this->logMessages as $message ) {
                    $this->log($message[0], $message[1]);
                }
                $this->logMessages = null;
            }
        }
    }
    protected function log($message, $level = 'info') {
        if ( $this->loggingInitialized ) {
            if ( $this->logger !== null ) {
                $this->logger->$level($message);
            }
        } else {
            $this->logMessages[] = array($message, $level);
        }
    }
    protected function logTrace($message) {
        return $this->log($message, 'trace');
    }
    protected function logDebug($message) {
        return $this->log($message, 'debug');
    }
    protected function logInfo($message) {
        return $this->log($message, 'info');
    }
    protected function logWarn($message) {
        return $this->log($message, 'warn');
    }
    protected function logError($message) {
        return $this->log($message, 'error');
    }
    protected function logFatal($message) {
        return $this->log($message, 'fatal');
    }

    public function getRegisteredObjectNames() {
        return array_keys($this->objectDefinitions);
    }

    public function getRegisteredObjectImplementationMap() {
        return $this->mapByInterface;
    }

    public function getObjectDefinition($name) {
        if ( ! isset($this->objectDefinitions[$name]) ) {
            throw new Exception('Could not locate object definition for name "' . $name . '"');
        }
        return $this->objectDefinitions[$name];
    }

    protected function replacePlaceholder($value) {
        if ( is_object($value) and $value instanceof substrate_ContextObjectReference ) {
            $value->setName($this->replacePlaceholderValue($value->getName()));
        } else if ( is_array($value) ) {
            foreach ( $value as $i => $v ) {
                $value[$i] = $this->replacePlaceholder($v);
            }
        } else if ( is_string($value) ) {
            $value = $this->replacePlaceholderValue($value);
        }
        return $value;
    }

    protected function replacePlaceholderValue($value) {
        if ( $this->getPlaceholderConfigurer() === null ) {
            // Just in case...
            return $value;
        } else {
            return $this->getPlaceholderConfigurer()->replacePlaceholders($value);
        }

    }

    protected function getPlaceholderConfigurer() {

        if ( $this->exists('placeholderConfigurer') ) {
            return $this->get('placeholderConfigurer');
        }

        return null;

    }

}

?>
